<template>
  <div class="main">
    <div class="wrapper">
      <div class="left">
        <el-upload
          class="upload-demo"
          name="file"
          drag
          :multiple="false"
          :show-file-list="false"
          :auto-upload="false"
          :on-change="handleFileChange"
        >
          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
          <div>请选择要上传的文件</div>
        </el-upload>
      </div>
      <div class="right">
        <div class="title">文件列表</div>
        <div class="container">
          <ul>
            <li v-for="(item, index) in fileList" :key="index">
              <div class="fileName">{{ item.fileName }}</div>
              <div class="fileSize">
                <!-- {{ (item.fileSize / 1024 / 1024).toFixed(2) }} MB -->
                {{ formatFileSize(item.fileSize) }}
              </div>
              <div class="process">
                <!-- 进度条 -->
                <div class="process-box">
                  <div
                    class="process-bar"
                    :style="{ width: item.progress + '%' }"
                  ></div>
                </div>
                <!-- 进度条上的文字 -->
                <div class="process-text">{{ item.progress }}%</div>
              </div>
              <div class="option">
                <div>
                  <el-button
                    type="info"
                    plain
                    @click="poolUpladFileHandler(item)"
                    >普通上传</el-button
                  >
                </div>
                <div>
                  <el-button
                    type="primary"
                    plain
                    @click="uploadFileHandler(item)"
                    >分片上传</el-button
                  >
                </div>
                <div>
                  <el-button type="warning" plain @click="PauseUploadFile(item)"
                    >暂停上传</el-button
                  >
                </div>
                <div>
                  <el-button
                    plain
                    type="success"
                    @click="downloadFileHandler(item.objectName)"
                    >下载</el-button
                  >
                </div>
                <div>
                  <el-button
                    plain
                    type="danger"
                    @click="deleteFileHandler(item.objectName)"
                    >删除</el-button
                  >
                </div>
              </div>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";
import {
  poolUploadFile,
  chunkUploadFile,
  mergeChunks,
  getFileList,
  downloadFile,
  deleteFile,
} from "../http/httpRequest";
import { calculateMD5, formatFileSize } from "../utils/fileUtil";

/**
 * 普通上传文件所需要的参数
 * 1、file：    要上传的文件
 * 2、userId：  当前用户ID
 *
 *
 * 分块上传文件所需要的参数
 * 1、file：      要上传的文件
 * 2、userId：    当前用户ID
 * 3、fileName：  文件名
 * 4、fileMd5:    文件的md5值
 * 5、chunkIndex：当前分块下标
 *
 *
 * 文件展示所需要的参数
 * 1、fileName：    文件名
 * 2、fileSize：    文件总大小
 * 2、progress:     当前文件上传进度
 * 3、isUploading:  是否正在上传
 * 4、chunkSize:    分块大小
 */
const fileList = ref([]); // 上传文件列表

const chunkSize = 5 * 1024 * 1024; // 5MB的分片

// 文件发生变化时触发
const handleFileChange = (uploadFile, uploadFileList) => {
  console.log("文件发生变化时触发：");
  console.log(uploadFile);
  // console.log(uploadFileList);

  // fileList.value.push({
  //   file: uploadFile.row, // 文件
  //   progress: 0, // 当前文件上传进度
  //   status: '', // 准备上传、上传中、上传完毕
  //   isUploading: false, // 是否正在上传
  //   chunkSize: chunkSize, // 分块大小
  //   id: Date.now() // 添加唯一标识
  // })

  // 将选择好的文件加入到文件列表中
  fileList.value = [
    ...fileList.value,
    {
      file: uploadFile.raw, // 文件
      fileName: uploadFile.raw.name, // 文件名
      fileSize: uploadFile.raw.size, // 文件大小

      chunkSize: chunkSize, // 分块大小
      totalChunks: 0, // 当前文件经过分块后得到的总块数
      progress: 0, // 当前文件上传进度
      isUploading: false, // 是否正在上传
      fileMd5: "", // 文件的md5值
      objectName: "", // 文件在minio中存储路径
      uploadedChunks: 0, // 前端页面中记录已经上传好的分块数量
    },
  ];

  // fileList.value = uploadFileList.map(file => ({
  //   file: file.raw,
  //   progress: 0,
  //   status: '准备上传',
  //   isUploading: false,
  //   chunkSize: chunkSize,
  //   id: Date.now() + Math.random()
  // }))
};

// 上传文件到minio 普通上传版本
const poolUpladFileHandler = async (item) => {
  console.log("当前要普通上传的文件为：");
  console.log(item);
  try {
    let res = await poolUploadFile({
      file: item.file,
      userId: "123",
    });
    console.log(res.data.data);
    item.progress = 100;
    item.objectName = res.data.data;
  } catch (error) {
    console.log(error);
    console.log("文件上传失败！");
  }
};

// 上传文件到minio 分片上传版本
const uploadFileHandler = async (item) => {
  // console.log("当前要分片上传的文件为：");
  // console.log(item);

  if (item.fileMd5 === "") {
    // 说明当前文件的md5值还没计算过，需计算，否则说明当前文件的md5值已经得到过，无需重新计算了！
    // 计算得到文件的MD5值
    console.log("开始计算当前文件的Md5值");
    const fileMd5 = await calculateMD5(item.file);
    console.log("当前文件的MD5值为:", fileMd5); // c9e3d01f50dfdf86694709390ae21631
    item.fileMd5 = fileMd5;
  }

  // 修改文件的上传状态，表示开始分块上传当前文件，用户可以通过点击“暂停上传”停止当前上传过程
  item.isUploading = true;

  // 计算当前文件分块后的总块数
  item.totalChunks = Math.ceil(item.file.size / item.chunkSize); // Math.ceil：向上取整

  // 分块上传（从上次上传的那一块开始上传）
  // for (let i = 0; i < item.totalChunks; i++) {
  for (let i = item.uploadedChunks; i < item.totalChunks; i++) {
    // 如果用户点击了“暂停上传”，则停止当前上传
    if (!item.isUploading) {
      return;
    }

    // 分块
    // 当前块数的起始位置
    const start = i * item.chunkSize;
    // 当前块数的结束位置
    const end = Math.min(start + item.chunkSize, item.fileSize);
    // 取得分块
    const chunk = item.file.slice(start, end);
    console.log("当前块数：" + i + "下方为当前分块后的Blob文件: ");
    console.log(chunk);

    // 准备分块上传文件的请求参数
    const formData = new FormData();
    formData.append("file", chunk); // 分好块的文件
    formData.append("userId", "123"); // 用户Id
    formData.append("chunkIndex", i); // 当前分块的块数
    formData.append("fileName", item.fileName); // 原文件名
    formData.append("fileMd5", item.fileMd5); // 文件的Md5值

    // 上传分块到minio
    try {
      let res = await chunkUploadFile(formData);
      console.log("文件的当前分块上传成功", res.data);

      // 更新进度条
      // 更新总上传字节数（5MB, 10MB......）
      let uploadedSize = (i + 1) * item.chunkSize;
      // 计算整体进度百分比
      item.progress = Math.min(
        100,
        Math.round((uploadedSize / item.fileSize) * 100) // Math.round：返回传入数值四舍五入后的整数
      );
    } catch (error) {
      console.error("第" + i + "块分块上传失败:", error);
      return;
    } finally {
      // 在 JavaScript 中，即使 catch 块中有 return 语句，finally 块仍然会执行。finally 块的执行发生在 return 之前。
      // 更新已经上传好的块数
      item.uploadedChunks = i;
    }
  }

  // 文件分块上传完毕后，应该去调用分块文件合并接口
  try {
    let res = await mergeChunks({
      userId: "123",
      fileMd5: item.fileMd5,
      fileName: item.fileName,
      totalChunks: item.totalChunks,
    });
    console.log("文件合并成功:", res.data.data);
    item.objectName = res.data.data; // 将文件在minio中存储的文件名存储
  } catch (error) {
    console.error("文件合并失败:", error);
  }
};

// 暂定当前文件的分块上传
const PauseUploadFile = (item) => {
  item.isUploading = false;
};

// 文件下载
const downloadFileHandler = async (objectName) => {
  try {
    let response = await downloadFile(objectName);

    // 动态创建隐藏的<a>标签，即创建下载连接，触发文件下载
    /**
     * window.URL.createObjectURL():
          创建一个指向内存中Blob对象的临时URL
          格式如：blob:http://localhost:8080/550e8400-e29b-41d4-a716-446655440000
          这个URL只在当前文档生命周期内有效

      new Blob([response.data]):
          将axios返回的二进制数据包装为Blob对象
          数组形式允许未来合并多个Blob部分   

      创建<a>标签:
          动态创建不可见的链接元素
          设置其href为刚创建的Blob URL    
     *  */
    const url = window.URL.createObjectURL(new Blob([response.data]));
    const link = document.createElement("a");
    link.href = url;

    // 文件名处理逻辑
    // 从响应头获取文件名
    /**
     * Content-Disposition响应头:
        后端设置的响应头，格式如：attachment; filename="example.pdf"
        比前端传递的objectName更可靠(可能被后端修改)

      正则表达式解析:
        匹配模式：filename=后面跟着用引号包裹的内容或没有引号的内容
        分解：
          filename[^;=\n]*= 匹配"filename="及可能存在的空格
          ((['"]).*?\2|[^;\n]*) 匹配两种形式：
            引号包裹的内容：(['"]).*?\2
            无引号的内容：[^;\n]*
        示例匹配结果：
          对于filename="文件 1.pdf" → 匹配组1为"文件 1.pdf"
        清理引号:
          replace(/['"]/g, '') 移除文件名两端的引号
     */
    const contentDisposition = response.headers["content-disposition"];
    let fileName = objectName;
    if (contentDisposition) {
      const fileNameMatch = contentDisposition.match(
        /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
      );
      if (fileNameMatch && fileNameMatch[1]) {
        // 优先使用后端返回的文件名
        fileName = fileNameMatch[1].replace(/['"]/g, "");
      }
    }

    console.log("响应中的文件名：" + fileName);

    // 触发下载
    /**
     * download属性:
        指定浏览器应下载该链接而非导航到它
        同时指定下载时的默认文件名
      DOM操作:
        必须将链接添加到文档中(appendChild)才能工作
        click() 方法程序化地触发点击事件
     */
    link.setAttribute("download", fileName);
    document.body.appendChild(link);
    link.click();

    // 资源清理
    /**
     * revokeObjectURL():
        释放Blob URL占用的内存，避免单页应用中的内存累积
        防止内存泄漏
        调用后URL立即失效
      移除链接元素:
        及时清理创建的临时DOM元素
        保持页面整洁
     */
    window.URL.revokeObjectURL(url);
    link.remove();
  } catch (error) {
    console.error("下载文件失败:", error);
  }
};

// 文件删除
const deleteFileHandler = async (objectName) => {
  let res = await deleteFile(objectName);
  if (res.data.code === 200) {
    console.log(res.data.data);
    getFileListHandler();
  } else {
    console.log(res.data.data);
  }
};

// 获取当前用户的文章列表
const getFileListHandler = async () => {
  let res = await getFileList("123");
  for (let i = 0; i < res.data.data.length; i++) {
    // 从后端获取到的文件需要添加一些属性
    res.data.data[i].progress = 100; // 表明上传完毕
  }
  fileList.value = res.data.data;
};

// 组件挂载完毕，获取当前用户的文章列表
onMounted(() => {
  getFileListHandler();
});
</script>

<style scoped>
.main {
  height: 100vh;
  width: 100%;
  background-color: rgb(222, 222, 243);
  padding: 100px 120px;
}

.main .wrapper {
  display: flex;
  justify-content: space-between;
  height: 100%;
  border-radius: 10px;
  background-color: aliceblue;
}

/* 左侧文件上传区域 */
.main .left {
  /* background-color: aqua; */
  height: 100%;
  display: flex;
  align-items: center;
  padding: 0 30px;
}

:deep(.upload-demo) {
  width: 150px;
}

/* 右侧文件列表 */
.main .right {
  /* background-color: green; */
  flex: 1;
  height: 100%;
  width: 100%;
  padding: 20px;
}

.main .right .title {
  width: 100%;
  font-size: 24px;
  padding-bottom: 10px;
  border-bottom: 1px solid black;
}

.main .right .container {
  padding-top: 20px;
  width: 100%;
}

/* 具体每一个文件 */
.main .right .container ul li {
  height: 40px;
  display: flex;
  align-items: center;
  margin-bottom: 20px;
}

.main .right .container ul li .fileName {
  width: 250px;
  white-space: nowrap; /* 禁止文字换行 */
  overflow-y: auto; /* 垂直方向隐藏溢出内容 */
  margin-right: 20px;
}

.main .right .container ul li .fileSize {
  width: 100px;
  margin-right: 20px;
}

/* 每一个文件的进度条 */
.main .right .container ul li .process {
  width: 300px;
  height: 100%;
  margin-right: 20px;
  display: flex;
  align-items: center;
}

/* 进度条外部盒子 */
.main .right .container ul li .process .process-box {
  background-color: #d5e0db;
  transition: width 1s;
  display: flex;
  align-items: center;
  flex: 1;
  height: 10px;
  margin-right: 20px;
}

/* 真正的进度条 */
.main .right .container ul li .process .process-box .process-bar {
  background-color: rgb(102, 158, 241);
  max-width: 100%;
  /* 给进度条初始高度，否则后续变化宽度，进度套无法显示 */
  height: 100%;
}

.main .right .container ul li .process .process-text {
  text-align: center;
  color: #333;
  font-weight: bold;
  width: 70px;
}

/* 操作栏 */
.main .right .container .option {
  display: flex;
}

.main .right .container .option div {
  margin-right: 10px;
}
</style>